﻿using SQLite.CodeFirst;
using SQLite.CodeFirst.Statement;
using System;
using System.Data.Entity;
using System.Data.Entity.Core.Metadata.Edm;
using System.Data.Entity.Infrastructure;
using System.Data.Entity.Migrations.History;
using System.Data.Entity.ModelConfiguration.Conventions;
using System.IO;
using System.Linq;
using System.Reflection;

namespace Infrastructure.Libraries.Sqlite
{
    public class SqliteDropCreateDatabaseIfModelChanges<TContext> : IDatabaseInitializer<TContext> where TContext : DbContext
    {
        protected readonly DbModelBuilder ModelBuilder;
        protected readonly string DatabaseFilePath;

        public const string DefaultTableName = "__MigrationHistory";
        private const string DataDirectoryToken = "|datadirectory|";

        internal const int ContextKeyMaxLength = 300;
        internal const int MigrationIdMaxLength = 150;

        public SqliteDropCreateDatabaseIfModelChanges(string connectionString, DbModelBuilder modelBuilder)
        {
            DatabaseFilePath = ConnectionStringParse(connectionString);
            ModelBuilder = modelBuilder;

            // This convention will crash the SQLite Provider before "InitializeDatabase" gets called.
            // See https://github.com/msallin/SQLiteCodeFirst/issues/7 for details.
            modelBuilder.Conventions.Remove<TimestampAttributeConvention>();
            ConfigMigrationHistory(modelBuilder);
        }

        private string ConnectionStringParse(string connectionString)
        {
            var path = connectionString.Trim(' ', ';').Split(';').FirstOrDefault(o => o.StartsWith("data source", StringComparison.OrdinalIgnoreCase)).Split('=').Last().Trim();
            if (!path.StartsWith("|datadirectory|", StringComparison.OrdinalIgnoreCase))
            {
                return path;
            }
            string fullPath;

            // find the replacement path
            object rootFolderObject = AppDomain.CurrentDomain.GetData("DataDirectory");
            string rootFolderPath = (rootFolderObject as string);
            if (rootFolderObject != null && rootFolderPath == null)
            {
                throw new InvalidOperationException("The value stored in the AppDomains 'DataDirectory' variable has to be a string!");
            }
            if (string.IsNullOrEmpty(rootFolderPath))
            {
                rootFolderPath = AppDomain.CurrentDomain.BaseDirectory;
            }

            // We don't know if rootFolderpath ends with '\', and we don't know if the given name starts with onw
            int fileNamePosition = DataDirectoryToken.Length;    // filename starts right after the '|datadirectory|' keyword
            bool rootFolderEndsWith = (0 < rootFolderPath.Length) && rootFolderPath[rootFolderPath.Length - 1] == '\\';
            bool fileNameStartsWith = (fileNamePosition < path.Length) && path[fileNamePosition] == '\\';

            // replace |datadirectory| with root folder path
            if (!rootFolderEndsWith && !fileNameStartsWith)
            {
                // need to insert '\'
                fullPath = rootFolderPath + '\\' + path.Substring(fileNamePosition);
            }
            else if (rootFolderEndsWith && fileNameStartsWith)
            {
                // need to strip one out
                fullPath = rootFolderPath + path.Substring(fileNamePosition + 1);
            }
            else
            {
                // simply concatenate the strings
                fullPath = rootFolderPath + path.Substring(fileNamePosition);
            }
            return fullPath;
        }

        private void ConfigMigrationHistory(DbModelBuilder modelBuilder)
        {
            modelBuilder.Entity<HistoryRow>().ToTable(DefaultTableName);
            modelBuilder.Entity<HistoryRow>().HasKey(
                h => new
                {
                    h.MigrationId,
                    h.ContextKey
                });
            modelBuilder.Entity<HistoryRow>().Property(h => h.MigrationId).HasMaxLength(MigrationIdMaxLength).IsRequired();
            modelBuilder.Entity<HistoryRow>().Property(h => h.ContextKey).HasMaxLength(ContextKeyMaxLength).IsRequired();
            modelBuilder.Entity<HistoryRow>().Property(h => h.Model).IsRequired().IsMaxLength();
            modelBuilder.Entity<HistoryRow>().Property(h => h.ProductVersion).HasMaxLength(32).IsRequired();
        }

        public string GetSql(DbModel model)
        {
            Assembly asm = Assembly.GetAssembly(typeof(SqliteInitializerBase<>));
            Type builderType = asm.GetType("SQLite.CodeFirst.Builder.CreateDatabaseStatementBuilder");

            ConstructorInfo builderConstructor = builderType.GetConstructor(new Type[] { typeof(EdmModel) });
            Object builder = builderConstructor.Invoke(new Object[] { model.StoreModel });

            MethodInfo method = builderType.GetMethod("BuildStatement", BindingFlags.Instance | BindingFlags.InvokeMethod | BindingFlags.Public);

            var statement = (IStatement)method.Invoke(builder, new Object[] { });
            string sql = statement.CreateStatement();
            return sql;
        }

        public void InitializeDatabase(TContext context)
        {
            var model = ModelBuilder.Build(context.Database.Connection);
            var sqliteDatabaseCreator = new SqliteDatabaseCreator(context.Database, model);
            var newSql = this.GetSql(model);

            bool dbExists = File.Exists(DatabaseFilePath);
            if (dbExists)
            {
                var oldSql = System.Text.Encoding.UTF8.GetString(context.Set<System.Data.Entity.Migrations.History.HistoryRow>().AsNoTracking().FirstOrDefault().Model);
                context.Database.Connection.Close();
                GC.Collect();
                if (oldSql == newSql)
                {
                    return;
                }
                for (int i = 0; i < 10; i++)
                {
                    try
                    {
                        File.Delete(DatabaseFilePath);
                        break;
                    }
                    catch (Exception)
                    {
                        System.Threading.Thread.Sleep(1);
                    }
                }
            }
            using (var transaction = context.Database.BeginTransaction())
            {
                try
                {
                    sqliteDatabaseCreator.Create();
                    transaction.Commit();
                }
                catch (Exception)
                {
                    transaction.Rollback();
                    throw;
                }
            }

            using (var transaction = context.Database.BeginTransaction())
            {
                try
                {
                    context.Set<HistoryRow>().Add(
                    new HistoryRow
                    {
                        MigrationId = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss:fffffff"),
                        ContextKey = context.GetType().FullName,
                        Model = System.Text.Encoding.UTF8.GetBytes(newSql.ToCharArray()),
                        ProductVersion = "6.1.3"
                    });
                    Seed(context);
                    context.SaveChanges();
                    transaction.Commit();
                }
                catch (Exception)
                {
                    transaction.Rollback();
                    throw;
                }
            }
        }

        protected virtual void Seed(TContext context)
        {
        }
    }
}